Genesis 3D |
||
per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto! |
Decima
lezione
Per commentare, dare suggerimenti
o consigli (molto apprezzati) e per segnalare eventuali errori o
disattenzioni (sempre possibili), puoi inviare una email all'autore (clicca
sul nome sopra). Grazie per la collaborazione!
Come creare un progetto con Visual C++ 6.0 per utilizzare le librerie
genesis 3D.
In questo tutorial creiamo il primo programma che utilizza le librerie
Genesis per aprire una finestra in fullscreen, caricare una mappa e orientare
la telecamera verso un punto.
La prima cosa da fare per creare un progetto con Visual C++ 6.0 e
creare una sottocartella nella propria area di lavoro nella quale copiare le
directory "lib", "include" presenti nella directory
Genesis, e creare due nuove cartelle: "levels" e "actor".
Copiare i file .dll presenti nella directory genesis nella cartella
creata. Copiare il file .bsp relativo alla mappa che è stata creata nella
directory levels.
La struttura finale della nostra cartella "tesina" sarà:
Apriamo Visual C++ e creiamo un nuovo progetto
di tipo "Win 32 Application", posizioniamo il progetto nella directory
appena creata e diamo il nome desiderato.
Alla domanda successiva rispondere "Empty project".
Il wizard ci presenterà un progetto vuoto. La prima cosa da fare è
quella di includere le librerie di genesis. Scegliere
"Project" "Add to Project" e "Files". Selezionare la cartella Lib e cambiare il
filtro in modo da visualizzare i file .lib; selezionare il file genesis.lib e
confermare.
Adesso dobbiamo creare un nuovo file .c; a tal proposito bisogna subito
fare una precisazione. Le librerie Genesis sono scritte in C e il VC++ compila
in modo diverso file di tipo .cpp e file di tipo .c; per non avere alcun
problema di compilazione è meglio utilizzare il linguaggio C.
Visual C++ non permette di creare file .c, quindi questi dovranno
essere creati esternamente e poi inclusi (con la stessa procedura con cui
abbiamo incluso le librerie).
Creiamo un file main.c e un file main.h e importiamoli nel progetto.
Nel file main.c aggiungere le righe:
#include <windows.h>
#include
"include\genesis.h"
#include "main.h"
La prima serve per utilizzare le strutture e le funzioni di windows per
aprire una finestra. La seconda informa il compilatore che i prototipi delle
strutture e delle funzioni genesis si trovano nella directory include nel file
genesis.h.
La terza riga fa si che nel file main.h si possano mettere strutture
dati e prototipi relativi al progetto.
#ifndef MAIN_H
#define MAIN_H
//Includes
#include <windows.h> //Always
include this before genesis.h
#include "include\genesis.h" //The
Genesis SDK include header
enum stato_giocatore { standing, walinkg, running, jumping,
jumping_down, falling };
enum direzione { UP, FRONT, LEFT };
A parte le direttive di inclusione che sono simili a quelle
appena viste, i due tipi enumerativi appena creati serviranno in seguito,
quando faremo muovere la telecamera nell'ambiente. Il primo indica lo stato del
giocatore, ovvero se stiamo camminando, correndo, saltando ecc...
La seconda serve per specificare un vettore. Vedremo
poi.
typedef struct {
char driver; //Driver we want to use
int Width; //Width of
the mode
int Height; //Height of
the mode
int keys[256]; // Array
Used For The Keyboard Routine
} InputOutput;
typedef struct {
geVec3d posizione; //
Posizione del giocatore
geVec3d angolo; //
direzione sguardo
geVec3d Mins; // Bounding box
geVec3d Maxs;
int height; //how
tall are we
int speed; //how
fast are we
int caduta; //
velocità di caduta
int
salto; // altezza del
salto
int
gradino; // altezza
massima del gradino che può salire
int stato;
int counter;
} Player;
// global definition
Player pl; //
variabile relativa al giocatore
InputOutput io; // parametri
usati per input/output
Queste due strutture dati, di cui creiamo una istanza
contengono informazioni essenziali all'animazione. Player contiene i parametri
del giocatore, tipo ingombro e velocità nella corsa. InputOutput contiene
informazioni sulla modalità video e sullo stato della tastiera.
HWND hWnd; //The window
geEngine *Engine; //The engine object
geSound_System *SoundSys; //The Genesis Sound System
geCamera *Camera; //The camera object
geWorld *World; //World Object
Queste variabili globali servono per la gestione
dell'engine di genesis.
// main headers
LRESULT CALLBACK WndProc(HWND
hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE
hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd);
void
CreateMainWindow(HINSTANCE hInstance, int nShowCmd);
void Shutdown();
void MainLoop();
geBoolean InitEngine(HWND
hWnd);
void
FindDriver(geDriver_System *DrvSys);
void LoadPrefs(char *drv, int
*width, int *height);
void LoadLevel(char
*FileName);
#endif
Infine sono elencati i prototipi delle funzioni che andremo a creare
nel file main.c.
La funzione main risulta ottima per capirei passi della creazione
dell'applicazione.
int WINAPI WinMain(HINSTANCE
hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
LoadPrefs(&io.driver, &io.Width, &io.Height);
CreateMainWindow(hInstance,nShowCmd);
//Initialize the Genesis engine
InitEngine(hWnd);
MainLoop();
//Shutdown the Genesis engine and clean up the memory
Shutdown();
//End Program
return 1;
}
La prima cosa che viene fatta è quella di leggere da un file di
configurazione qual'è la modalità video prescelta. In questo modo
l'applicazione può essere configurata esternamente.
La seconda istruzione crea la finestra entro la quale deve girare
l'applicazione genesis.
InitEngine è la funzione della quale parleremo più in dettaglio in
questa lezione. Essa si occupa di inizializzare il motore grafico.
MainLoop è una funzione che effettua il rendering grafico dell'ambiente.
Shutdown di occupa di deallocare le risorse impiegate in modo da poter
terminare correttamente il programma.
Tale funzione presume che nella directory principale del progetto sia
presente un file di configurazione in formato txt chiamato
"video.cfg". Il file deve presentare tre righe. Nella prima va
specificato il driver da usare, nelle altre due la risoluzione.
Il driver deve essere specificato nel formato seguente:
( = D3D
G = Glide
S = Software
Un esempio di un possibile contenuto del file è:
(
640
480
che indica che si usa il driver per le DirectX con la modalità video
640 x 480.
void LoadPrefs(char *drv, int *width, int *height)
{
FILE *f;
//Open the minapp.cfg file
if ((f =
fopen("video.cfg", "r+")) == NULL)
{
MessageBox(hWnd, "Failed to read viedo.cfg", "No
Prefs", MB_OK);
_exit(-1);
}
//read in the driver
fscanf(f, "%s",
drv);
//read in the width
fscanf(f, "%d",
width);
//read in the height
fscanf(f, "%d",
height);
//close the file
fclose(f);
}
La funzione è abbastanza semplice: i valori vengono letti e passati per
riferimento alla funzione chiamante.
Non mi soffermerò sul significato delle istruzioni presenti in questa
funzione, in quanto esula dallo scopo di questo tutorial. Consultare un manuale
sulla programmazione delle win32 per una maggiore comprensione.
void
CreateMainWindow(HINSTANCE hInstance, int nShowCmd) {
WNDCLASS wc;
DWORD dwExStyle; // Window Extended Style
DWORD dwStyle; // Window Style
RECT WindowRect; // Grabs Rectangle Upper Left /
Lower Right Values
WindowRect.left=(long)0; //
Set Left Value To 0
WindowRect.right=(long)io.Width; // Set Right Value To Requested Width
WindowRect.top=(long)0; //
Set Top Value To 0
WindowRect.bottom=(long)io.Height; // Set Bottom Value To Requested Height
//Initialize the window class
hInstance =
GetModuleHandle(NULL); //
Grab An Instance For Our Window
wc.style =
CS_HREDRAW | CS_VREDRAW | CS_OWNDC; //
Redraw On Size, And Own DC For Window.
wc.lpfnWndProc =
(WNDPROC) WndProc; //
WndProc Handles Messages
wc.cbClsExtra =
0; //
No Extra Window Data
wc.cbWndExtra =
0; //
No Extra Window Data
wc.hInstance =
hInstance; //
Set The Instance
wc.hCursor = LoadCursor(NULL,
IDC_ARROW); // Load
The Arrow Pointer
wc.lpszMenuName =
NULL; //
We Don't Want A Menu
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
wc.lpszClassName = "MinApp";
//Register the window class
RegisterClass(&wc);
dwExStyle=WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; // Window Extended Style
dwStyle=WS_OVERLAPPEDWINDOW; //
Windows Style
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle); // Adjust Window To True Requested
Size
//Create the window
hWnd=CreateWindowEx( dwExStyle, // Extended Style For The Window
wc.lpszClassName,
"Tesina
di Informatica Grafica",
dwStyle | //
Defined Window Style
WS_CLIPSIBLINGS | // Required Window Style
WS_CLIPCHILDREN, // Required Window Style
0,
0, // Window Position
WindowRect.right-WindowRect.left, // Calculate Window Width
WindowRect.bottom-WindowRect.top, // Calculate Window Height
NULL, // No Parent Window
NULL, // No Menu
hInstance, // Instance
NULL); // Dont Pass Anything To
WM_CREATE
if (!hWnd)
{
MessageBox(NULL, "Could not create main
window!", "MinApp Error...", 48);
_exit(-1);
}
//Make the window appear and update it
ShowWindow(hWnd, nShowCmd);
UpdateWindow(hWnd);
}
L'unica cosa da tenere in conto è il puntatore hWnd alla
struttura HWND di windows che serve come "handler" della viewport che
abbiamo creato. Questo parametro sarà necessario per inizializzare il motore
grafico.
Mediante questa funzione rileviamo i messaggi di windows
che ci interessano. Nel nostro caso questa funzione ci servirà per la lettura
della tastiera. A tale scopo abbiamo un vettore di 256 elementi con il quale
mappiamo i tasti. Ogni elemento riposta lo stato del tasto. TRUE = premuto,
FALSE = rilasciato.
LRESULT CALLBACK WndProc( HWND hWnd, // Handle For This Window
UINT uMsg, // Message
For This Window
WPARAM wParam, //
Additional Message Information
LPARAM lParam) //
Additional Message Information
{
switch (uMsg){ //
Check For Windows Messages
case WM_CLOSE: //
Did We Receive A Close Message?
{
PostQuitMessage(0); //
Send A Quit Message
return 0; //
Jump Back
}
case WM_KEYDOWN: //
Is A Key Being Held Down?
{
io.keys[wParam] = TRUE; // If So, Mark It As TRUE
return 0; //
Jump Back
}
case WM_KEYUP: //
Has A Key Been Released?
{
io.keys[wParam] = FALSE; // If So, Mark It As FALSE
return 0; //
Jump Back
}
}
// Pass All Unhandled Messages To DefWindowProc
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
Come abbiamo annunciato questa è la funzione sulla quale ci
soffermeremo più a lungo. Non è particolarmente complicata, ma da modo di
spiegare alcuni concetti base di genesis.
geBoolean InitEngine(HWND
hWnd)
{
GE_Rect Rect; //
rettangolo di viewport
geDriver_System *DrvSys; //
driver di sistema
geXForm3d ViewXForm;
Engine = geEngine_Create(hWnd, "Tesina",
".");
if (!Engine)
{
MessageBox(hWnd, "Could not create engine
object!", "MinApp Error...", 48);
return GE_FALSE;
}
Ecco qui l'istruzione più importante di tutto il programma.
La creazione dell'engine, ovvero il motore grafico. Engine è un puntatore alla
struttura geEngine, che funge da wrappler per tutte
le altre stutture di genesis.
Ecco come si pronuncia la documentazione originale di
genesis in proposito:
"The geEngine
object is the container for the geDriver_System object, the geWorld object, and
the geBitmap object. The primary job of the geEngine object is to provide a
fast, and efficient interface to the output device, via the geDriver_System
object."
In sostanza geEngine ci permette di manipolare oggetti come il World
(che comprende la geometria dell'ambiente, degli actors, le telecamere, le luci
ecc...) i driver e gli oggetti 2D.
geEngine_EnableFrameRateCounter(Engine, FALSE);
Questa istruzione abilita o disabilita la visualizzazione
dei parametri di rendering. Può essere utile attivarla per capire il concetto.
//This function will enable you to use sound with your program through
the Genesis API.
SoundSys = geSound_CreateSoundSystem(hWnd);
if (!SoundSys)
MessageBox(hWnd, "Could not create Sound
System! There will be no sound!",
"No Sound...", 48);
Creiamo il driver per il suono.
DrvSys = geEngine_GetDriverSystem(Engine);
if (!DrvSys)
{
MessageBox(hWnd, "Could not get the Genesis Driver
System!", "MinApp Error...", 48);
return GE_FALSE;
}
Adesso che abbiamo un motore grafico dobbiamo settare i
suoi parametri di funzionamento. La prima cosa da fare è scegliere il driver da
utilizzare. A tale scopo si istanzia la variabile DrvSys che serve da
manipolatore del driver scelto. Quindi con la funzione FindDrivers si impostano
driver e modalità video secondo i parametri che abbiamo letto dal file di
configurazione, e secondo le disponibilità del sistema.
//Set the driver you want to use
FindDriver(DrvSys);
//Setup the camera rect
Rect.Left = 0; //The
far left of the screen
Rect.Right = io.Width - 1; //The
far right of the screen
Rect.Top = 0; //The
very top of the screen
Rect.Bottom = io.Height - 1; //The
very bottom of the screen
//Create the camera object.
The first parameter is the FOV (Field of View). 2.0f represents a 90
//degree FOV. The
second is the rectangle we just setup.
Camera = geCamera_Create(2.0f, &Rect);
//We can't work without a camera.
If it doesn't exist, exit the program
if (!Camera)
{
MessageBox(hWnd, "Could not create camera
object!", "MinApp Error...", 48);
_exit(-1);
}
Adesso creiamo una telecamera. I parametri per la creazione
sono: FOV (field of view), ovvero l'angolo di vista (2.0f corrisponde a circa 90
gradi) e Rect che è il rettangolo di viewport (in coordinate di schermo). Il
tipo di proiezione della telecamera è di tipo prospettico.
//Set up the XForm for the camera.
XForms determine your place in the world.
//Clear the transform
geXForm3d_SetIdentity(&ViewXForm);
//Setup the rotation
geXForm3d_RotateX(&ViewXForm, 0.0f);
geXForm3d_RotateY(&ViewXForm, 0.0f);
geXForm3d_RotateZ(&ViewXForm, 0.0f);
geXForm3d_Translate(&ViewXForm, 0.0f, 180.0f, 0.0f);
//Set the XForm to the camera
geCamera_SetWorldSpaceXForm(Camera,
&ViewXForm);
Adesso determiniamo la posizione della telecamera. Per
farlo analizziamo un utile strumento di genesis: geXForm3d.
Dalle specifiche leggiamo che "geXForm3d is a
transform object. It is used to specify orientations for
objects in a coordinate system, and to transform world coordinates to screen
coordinates for rendering."
Si tratta del wrappler di una matrice di trasformazione.
Impostiamo la matrice come identità e poi sommiamo la rotazione e la
traslazione.
Con il comando geCamera_SetWorldSpaceXForm applichiamo la
trasformazione risultante alla camera.
//Load the level
LoadLevel("levels\\esempio.bsp");
return GE_TRUE;
}
L'ultima operazione è quella di creare un oggetto geWorld,
demandato alla funzione LoadLevel(). Il parametro passato è una stringa con la
posizione del file della mappa da caricare.
Questa funzione si occupa di cercare il miglior driver che
il sistema ha a disposizione e che risponda alle caratteristiche selezionate
dall'utente nel file di configurazione.
void
FindDriver(geDriver_System *DrvSys) {
char *drvname;
int tempwidth;
int tempheight;
geDriver *Driver;
geDriver_Mode *Mode;
//Get the first driver
Driver = geDriver_SystemGetNextDriver(DrvSys, NULL);
Mediante l'istruzione geDriver_SystemGetNextDriver è
possibile scorrere i driver a disposizione. I driver sono rappresentati dai
file .dll presenti nella directory principale del sistema e sono di tre tipi:
DirectX, Glide e Software.
//Loop through the driver list and find the driver that matches what we
loaded into our variables before.
while (1)
{
//If there is no first driver then break the loop
if (!Driver)
{
MessageBox(hWnd, "Could not find a valid
driver!", "MinApp Error...", 48);
_exit(-1);
}
//Get the name of the driver
geDriver_GetName(Driver, &drvname);
//Check and see if the first character matches the driver
we loaded into our variable before. If
//it is, then break the loop.
if (drvname[0] == io.driver)
break;
//If not, get the next one
Driver = geDriver_SystemGetNextDriver(DrvSys, Driver);
}
Mediante questo loop scorriamo tutti i driver di sistema e
cerchiamo quello il cui nome corrisponde a quello settato nel file di
configurazione ('(', 'G', S'). Se nessun driver corrisponde usciamo dal
programma con un messaggio di errore.
Trovato il driver dobbiamo settare la modalità video.
Mediante il comando geDriver_GetNextMode scorriamo tutte le modalità messe a
disposizione dal driver.
//Get the first mode
Mode = geDriver_GetNextMode(Driver, NULL);
//Loop through the mode list and find the mode that matches the
width and height of our variables
while (1)
{
//If there is no mode, break the loop and exit the
program
if (!Mode)
{
MessageBox(hWnd, "Could not find a valid
mode!", "MinApp Error...", 48);
_exit(-1);
}
//Get the width and height of the mode
geDriver_ModeGetWidthHeight(Mode, &tempwidth,
&tempheight);
//See if the width and height match the variables. If it does, break the loop
if (tempwidth == io.Width && tempheight ==
io.Height)
break;
//If not, get the next mode
Mode = geDriver_GetNextMode(Driver, Mode);
}
Ogni modalità ottenuta viene confrontata con quelle
impostate e si esce da ciclo quando si trova quella che corrisponde. Se non
viene trovata nessuna corrispondenza si esce dal programma con un messaggio di
errore.
//We have the Driver and Mode we want, now tell the engine to start
if (!geEngine_SetDriverAndMode(Engine, Driver, Mode))
{
MessageBox(hWnd, "Could not start the engine!",
"MinApp Error...", 48);
_exit(-1);
}
}
Trovata la modalità video e il driver si imposta la scelta
effettuata con il comando geEngine_SetDriverAndMode.
Questa è la funzione che si occupa di effettuare il
rendering dell'ambiente e di interagire con l'utente. Attualmente l'unica
interazione avviene per terminare l'applicazione.
void MainLoop() {
MSG msg;
geBoolean run;
//Main loop
run = GE_TRUE;
while (run) {
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { // Is There A Message Waiting?
if (msg.message==WM_QUIT) { // Have We Received A Quit Message?
run = 0; //
If So done=TRUE
} else { //
If Not, Deal With Window Messages
TranslateMessage(&msg); // Translate The Message
DispatchMessage(&msg); // Dispatch The Message
}
} else { //
If There Are No Messages
if (!geEngine_BeginFrame(Engine, Camera, GE_FALSE))
run = 0;
if (!geEngine_RenderWorld(Engine, World, Camera, 0.0f))
run = 0;
if (!geEngine_EndFrame(Engine))
run = 0;
if (io.keys[VK_ESCAPE]) {
run=0;
}
}
}
}
La funzione consiste in un ciclo che dipende dalla
variabile booleana "run". Finché il valore della variabile rimane
true il ciclo continua. Attualmente un ciclo è composto da due parti.
La prima parte effettua una verifica dei messaggi in arrivo
all'applicazione e verifica se è presente il messaggio WM_QUIT che windows
manda quando una applicazione deve essere chiusa. In tal caso run viene posto a
false e si esce dal ciclo.
La seconda parte effettua il rendering del World. Le
operazioni di rendering possono essere effettuate solo tra le istruzioni di
geEngine_BeginFrame e geEngine_EndFrame.
L'errata conclusione di una di queste operazioni o la
pressione del tasto ESC fa uscire dal ciclo.
Scopo di tale funzione è di rilasciare le risorse che sono
state impegnate: World, Camera, SoundSys e Engine.
void Shutdown()
{
if (World)
{
geEngine_RemoveWorld(Engine, World);
geWorld_Free(World);
}
if (Camera)
geCamera_Destroy(&Camera);
if (SoundSys)
geSound_DestroySoundSystem(SoundSys);
if (Engine)
{
geEngine_ShutdownDriver(Engine);
geEngine_Free(Engine);
}
}
Come si può vedere i comandi essenziali per l'esecuzione di
una semplice applicazione dimostrativa "donothing" sono abbastanza
pochi e piuttosto semplici da intuire. Nelle prossime lezioni vedremo come
aggiungere interattività, permettendo alla telecamera di muoversi nell'ambiente
mediante uso combinato di tastiera e mouse.
Questo articolo è stato scaricato
dal Club di informatica |